﻿/*******************************************************************************************
 * Copyright (c) 2006-2009 Alex Robson, Rob Simmons, Craig Israel, and Rachel Twyford
 *
 * Use of this software in any form requires that you have both 
 * read and agreed to the following terms:
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions: 
 *
 * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, 
 * this list of conditions and the following disclaimer in the 
 * documentation and/or other materials provided with the distribution.
 * Neither the name of Nvigorate nor the names of its contributors may 
 * be used to endorse or promote products derived from this software without 
 * specific prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 * DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ****************************************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Nvigorate.Common;
using Nvigorate.Common.Reflection;
using Nvigorate.Relational.Mapping.Index;
using Nvigorate.Relational.Mapping.Interface;
using Nvigorate.Relational.Record;
using Nvigorate.Relational.Repository;

namespace Nvigorate.Relational.Binding
{
    internal class BindingLambdaFactory
    {
        #region Fields

        private static Func<Expression, Type, Expression> convert =
            (v, t) => Expression.Convert(v, t, TypeCaster.GetConvertMethodInfo(t));

        #endregion

        #region Public Methods

        /// <summary>
        /// This is the core entry point which ultimately creates the compiled binding function
        /// </summary>
        /// <typeparam name="T">The type being bound by this function</typeparam>
        /// <param name="source">The data source (connection string, file, etc.)</param>
        /// <param name="record">The 'seeder' record to base the expression tree on</param>
        /// <returns></returns>
        public static Func<IRecord, T> GetBindingLambda<T>(IBindingMap map, IRecord record) where T : new()
        {
            Type targetType = typeof(T);

            var mapExpression = Expression.Constant(map);

            ParameterExpression parameterExpression = Expression.Parameter(typeof(IRecord), "record");
            var bindings = new List<MemberBinding>();

            // Get bindings for assignments
            bindings.AddRange(
                map.GetSuppliedAssignments(record)
                    .Select(a => GetBindingExpressionForAssignment<T>(parameterExpression, a))
                    .Cast<MemberBinding>()
                );

            // Setup the object if it implements IPersistable
            if (Reflector.ImplementsInterface(targetType, typeof(IPersistable)))
            {
                bindings.Add(GetBindingExpressionForContributors<T>(parameterExpression, mapExpression));
                bindings.Add(Reflector.GetPropertyBindExpression(targetType, "ChangedValues", Expression.Constant(new List<string>())));
                bindings.Add(Reflector.GetPropertyBindExpression(targetType, "Keys", Expression.Constant(
                                                    map
                                                    .GetAssignmentsByPredicate(a => a.PrimaryKey)
                                                    .Select(a => a.Target.Field)
                                                    .ToList()
                                                 )));
            }

            MemberInitExpression initExpression = Expression.MemberInit(
                Expression.New(targetType),
                bindings);
            LambdaExpression lambdaExpression = Expression.Lambda(typeof(Func<IRecord, T>),
                                                                  initExpression,
                                                                  parameterExpression);

            return (Func<IRecord, T>)lambdaExpression.Compile();
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Retrieves the property info for the assignment
        /// </summary>
        /// <param name="target">The target business object type</param>
        /// <param name="assignment">The assignment to obtain the property from</param>
        /// <returns>PropertyInfo</returns>
        private static PropertyInfo GetTargetPropertyInfo(Type target, IAssignment assignment)
        {
            var property = Reflector.GetPropertyInfo(target, assignment.Target.Field, false);
            if (property == null)
                throw new ReflectorException("Type {0} does not contain a definition for property {1}",
                                             target.FullName, assignment.Target.Field);
            return property;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="parameterExpression"></param>
        /// <param name="assignment"></param>
        /// <returns></returns>
        private static System.Linq.Expressions.MemberAssignment GetBindingExpressionForAssignment<T>(
            ParameterExpression parameterExpression, IAssignment assignment)
        {
            Type targetType = typeof(T);
            Type propertyType = GetTargetPropertyInfo(targetType, assignment).PropertyType;

            Expression<Func<IRecord, object>> value1 = i => i[assignment.Target.Field];
            Expression castExpression = convert(Expression.Invoke(value1, parameterExpression), propertyType);

            return Reflector.GetPropertyBindExpression(targetType, assignment.Target.Field, castExpression);
        }

        /// <summary>
        /// 4
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="parameterExpression"></param>
        /// <param name="mapExpression"></param>
        /// <returns></returns>
        private static System.Linq.Expressions.MemberAssignment GetBindingExpressionForContributors<T>(
            ParameterExpression parameterExpression, Expression mapExpression)
        {
            Type targetType = typeof(T);
            Expression<Func<IRecord, IBindingMap, List<string>>> call = (r, m) => r.GetContributingSources(m);
            var caller = Expression.Invoke(call, parameterExpression, mapExpression);
            return Reflector.GetPropertyBindExpression(targetType, "Loaded", caller);
        }

        #endregion
    }
}
